#!/usr/local/bin/python3
# -*- coding: utf-8 -*-

"""
@File    : chart_wvt_plot.py
@Author  : Link
@Time    : 2024/8/6 22:05
@Mark    : 改造为Wave生成器
"""
from dataclasses import dataclass

import numpy as np
from PySide2.QtCore import Slot
from PySide2.QtGui import QPen, QColor
from pyqtgraph import PlotWidget, PlotDataItem, ViewBox, InfiniteLine, TextItem, AxisItem, ScatterPlotItem
from pyqtgraph.dockarea import Dock, DockArea

import pandas as pd
from math import ceil, log10

from FakerDevice.fakerFPGA.digi_variable import WaveTable, PinType
from FakerDevice.fakerFPGA.ui_component.ui_unit_chart import UnitChartWindow


def GraphRangeSignal(func):
    """
    这个是为了防止信号误触发
    :param func:
    :return:
    """

    def wrapper(ctx, *args, **kwargs):
        try:
            ctx.pw.sigRangeChanged.disconnect()
        except RuntimeError:
            pass
        if ctx.p_range is None:
            ctx.p_range = RangeData()
        res = func(ctx, *args, **kwargs)
        ctx.p_range.x_min, ctx.p_range.x_max = ctx.bottom_axis.range
        ctx.p_range.y_min, ctx.p_range.y_max = ctx.left_axis.range
        ctx.pw.sigRangeChanged.connect(ctx.set_range_data_to_chart)
        return res

    return wrapper


class AteCycleMode:
    X8 = 8
    X4 = 4
    X1 = 1


@dataclass
class RangeData:
    x_min: int = 0
    x_max: int = 0
    y_min: int = 0
    y_max: int = 0


class WvtData:
    YStep = 26
    list_bins = None  # 用来提升性能
    df: pd.DataFrame = None  # 全局数据
    f_df: pd.DataFrame = None  # FailCycle数据
    f_dfDict: dict = None  # FailCycle数据
    Remarks: dict = None  # 全局备注信息
    WvtPlotMap: dict = None  # 存放WvtPlot的Map
    fWvtPlotMap: dict = None  # 存放WvtPlot的Map
    rWvtPlotMap: dict = None  # 存放根据FailCycle算出来的实际的WvtPlot的Map
    GraphPlotScatterSimple = 10000
    RemarkLinesSimple = 50
    GraphPlotScopeMaxLineColumns = 1000
    PinOffset: dict = None
    PinList: dict = None
    fPinList: dict = None  # 用于绘图的PinList
    VecSuffix: str = "Cycle"  # Avc单位
    Suffix: str = ""  # Timing单位
    TimingSet: float = 1  # 一个Cycle的周期
    CycleMode: int = 1  # 在测试机的Cycle位置
    MaskNan: bool = True  # 是否将Mask改为NaN
    ShowFcRange: int = 5000

    waveTimingCycle = 20

    def __init__(self):
        self.list_bins = 5
        self.WvtPlotMap = dict()
        self.fWvtPlotMap = dict()
        self.rWvtPlotMap = dict()
        self.PinOffset = dict()
        self.PinList = dict()
        self.fPinList = dict()
        self.f_dfDict = dict()


class TimeAxis(AxisItem):
    TimingSet = 2

    def tickStrings(self, values, scale, spacing):
        """Return the strings that should be placed next to ticks. This method is called
        when redrawing the axis and is a good method to override in subclasses.
        The method is called with a list of tick values, a scaling factor (see below), and the
        spacing between ticks (this is required since, in some instances, there may be only
        one tick and thus no other way to determine the tick spacing)

        The scale argument is used when the axis label is displaying units which may have an SI scaling prefix.
        When determining the text to display, use value*scale to correctly account for this prefix.
        For example, if the axis label's units are set to 'V', then a tick value of 0.001 might
        be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and
        thus the tick should display 0.001 * 1000 = 1.
        """
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)

        places = max(0, ceil(-log10(spacing * scale)))
        strings = []
        for v in values:
            vs = v * scale
            if abs(vs) < .001 or abs(vs) >= 10000:
                vstr = "%g" % (vs * TimeAxis.TimingSet)
            else:
                vstr = ("%%0.%df" % (places * TimeAxis.TimingSet)) % vs
            strings.append(vstr)
        return strings


class WvtChart(UnitChartWindow):
    d: WvtData = None  #
    p_range: RangeData = None
    sig = None
    change: bool = False
    line_init: bool = False
    y_local_cache: dict = None

    def __init__(self):
        super(WvtChart, self).__init__()
        self.area = DockArea()
        self.setCentralWidget(self.area)

        self.sig = 0

        # Wvt绘制UI
        self.vb = ViewBox()
        self.vb.setMouseMode(ViewBox.RectMode)
        self.y_axis = TimeAxis(orientation="top", units='ns')
        self.pw = PlotWidget(viewBox=self.vb, enableMenu=False, axisItems={'top': self.y_axis})
        self.pw.hideButtons()
        self.pw.showAxes(True, showValues=(True, True, False, True))
        self.bottom_axis = self.pw.getAxis("bottom")
        self.left_axis = self.pw.getAxis("left")

        # # 信息打印LabelUI
        # self.label_widget = GraphicsLayoutWidget(self)
        # label = LabelItem(justify='right')
        # self.label_widget.addItem(label)
        # dock_label = Dock("", size=(400, 10))
        # dock_label.addWidget(self.label_widget)
        # self.area.addDock(dock_label, "top")

        # textItem浮动在Wvt中
        self.wvt_text: TextItem = None

        # LoggerUI, 用来输出Print

        # 使用dock来管理UI
        dock_wvt = Dock("WvtPlot", size=(400, 300))
        dock_wvt.addWidget(self.pw)
        self.area.addDock(dock_wvt, "right")  # , dock_label)

        self.d = WvtData()
        self.p_range = RangeData()

        self.y_local_cache = dict()
        self.chart_v_lines = []
        self.set_mouse_mode_button.pressed.connect(self._set_mouse_mode_button)

    @Slot()
    def _set_mouse_mode_button(self):
        if self.set_mouse_mode_button.isChecked():
            self.vb.setMouseMode(ViewBox.RectMode)
        else:
            self.vb.setMouseMode(ViewBox.PanMode)

    def set_default_range(self):
        self.p_range.x_min = 0
        self.p_range.x_max = len(self.d.df)

    def set_p_range(self, ax):
        self.p_range.y_min, self.p_range.y_max = ax[1][0], ax[1][1]
        self.p_range.x_min, self.p_range.x_max = ax[0][0], ax[0][1]

    def get_cache_range(self, sig):
        if sig == 1:  # 0 是 X, 1 是Y 的变化
            data_min, data_max = self.p_range.y_min, self.p_range.y_max
        else:
            data_min, data_max = self.p_range.x_min, self.p_range.x_max
        return data_min, data_max

    def init_movable_line(self):
        """
        指示线
        :return:
        """
        v_line = InfiniteLine(angle=90, movable=False,
                              labelOpts={"color": (255, 255, 255,), },  # 'fill': (0, 200, 0, 100)
                              label="{value:0.3f}" + self.d.VecSuffix, )
        self.vb.addItem(v_line, ignoreBounds=True)

        def mouseMoved(evt):
            if self.vb.sceneBoundingRect().contains(evt):
                mouse_point = self.vb.mapSceneToView(evt)
                v_line.setPos(mouse_point.x())
                if "REMARK" not in self.d.df:
                    return
                if self.wvt_text is None:
                    return
                int_x = int(mouse_point.x())
                ate_cycle, ate_cycle_div = divmod(int_x, self.d.CycleMode)
                remark = ""
                if 0 < int_x < len(self.d.df):
                    remark = self.d.df.iloc[int_x].REMARK
                self.wvt_text.setHtml(
                    html='<div style="text-align: center">'
                         '<span style="color: #FFF;">AvcCycle->{}</span>'
                         '<br>'
                         '<span style="color: #FFF;">AteCycle->{}:{}</span>'
                         '<br>'
                         '<span style="color: #FFF;">Time({})->{}</span>'
                         '<br>'
                         '<span style="color: #FFF;">REMARK->{}</span>'
                         '</div>'.format(int_x, ate_cycle, ate_cycle_div,
                                         self.d.Suffix, int_x * self.d.TimingSet,
                                         remark
                                         ),
                )

        self.vb.scene().sigMouseMoved.connect(mouseMoved)

    def dataPreAnalysis(self):
        """
        对Pin类型进行处理, 以及DataFrame进行操作, 方便区分出I/O/IO
        DataFrame要生成新的Wvt
        :return:
        """
        newPinList = {}
        for key, value in self.d.PinList.items():
            if value == PinType.IO:
                if self.d.MaskNan:
                    self.d.df.loc[self.d.df[key] == int(WaveTable.INPUT_X), key] = np.nan
                    self.d.df.loc[self.d.df[key] == int(WaveTable.OUTPUT_X), key] = np.nan

                i_key = key + "_I"
                o_key = key + "_O"
                newPinList[i_key] = PinType.IN
                newPinList[o_key] = PinType.OUT
                # 操作DataFrame
                self.d.df[i_key] = np.nan
                self.d.df[o_key] = np.nan

                self.d.df.loc[self.d.df[key] == int(WaveTable.INPUT_0), i_key] = int(WaveTable.INPUT_0)
                self.d.df.loc[self.d.df[key] == int(WaveTable.INPUT_1), i_key] = int(WaveTable.INPUT_1)
                if self.d.MaskNan:
                    self.d.df.loc[self.d.df[key] == int(WaveTable.INPUT_X), i_key] = np.nan
                self.d.df.loc[self.d.df[key] == int(WaveTable.OUTPUT_L), o_key] = int(WaveTable.OUTPUT_L)
                self.d.df.loc[self.d.df[key] == int(WaveTable.OUTPUT_H), o_key] = int(WaveTable.OUTPUT_H)
                if self.d.MaskNan:
                    self.d.df.loc[self.d.df[key] == int(WaveTable.OUTPUT_X), o_key] = np.nan

            if value == PinType.IN:
                if self.d.MaskNan:
                    self.d.df.loc[self.d.df[key] == int(WaveTable.INPUT_X), key] = np.nan
                newPinList[key] = value
            if value == PinType.OUT:
                if self.d.MaskNan:
                    self.d.df.loc[self.d.df[key] == int(WaveTable.OUTPUT_X), key] = np.nan
                newPinList[key] = value
        self.d.fPinList = newPinList

    def f_dataPreAnalysis(self):
        """
        处理Comment, 只显示OutPut
        f_df -> df["FailCycle"] = df["FailCycle"].astype(int) From analysis_93k_report_split_for_wvt_plot
        For Debug
        :return:
        """
        if self.d.f_df is None:
            return
        for (pin, df) in self.d.f_df.groupby("FailPin"):
            self.d.f_dfDict[pin] = df

    @GraphRangeSignal
    def fPlot(self):
        """
        将FailCycle的东西打印到WvtPlot中
        使用黄色线条来对FailCycle进行绘图, 另外使用散点图在原图上面叠加Fail位置
        :return:
        """
        if self.d.f_df is None:
            return
        for key, plot in self.d.fWvtPlotMap.items():
            plot.clear()
        int_x_min = int(self.p_range.x_min)
        int_x_max = int(self.p_range.x_max)
        y_range = self.p_range.y_max - self.p_range.y_min
        sym_size = 10
        if y_range < 200:
            sym_size = 18
        if y_range < 100:
            sym_size = 35

        for pin_key, df in self.d.f_dfDict.items():
            if pin_key not in self.d.PinList:
                continue
            if len(df) > 500:
                df = df[::self.d.list_bins]
            df = df[(df["FailCycle"] >= int_x_min) & (df["FailCycle"] <= int_x_max)]
            # pin_type = self.d.PinList[pin_key]
            # if pin_type == PinType.IO:
            #     pin_key = pin_key + "_O"
            y_index = self.y_local_cache[pin_key]
            scatter_key = "{}@{}".format(pin_key, "sca")
            x_data = np.array(df["FailCycle"]) + self.d.PinOffset.get(pin_key, 0)
            y_data = np.full(len(x_data), self.d.YStep * y_index + int(WaveTable.OUTPUT_X))
            if scatter_key not in self.d.fWvtPlotMap:
                color = (187, 26, 95, 70)
                plot = ScatterPlotItem(
                    pen=color, symbolPen='w', symbol='x'
                )
                plot.setData(
                    x=x_data,
                    y=y_data,
                    size=sym_size,
                )
                self.d.fWvtPlotMap[scatter_key] = plot
                self.pw.addItem(plot)
            else:
                plot = self.d.fWvtPlotMap[scatter_key]
                plot.setData(
                    x=x_data,
                    y=y_data,
                    size=sym_size,
                    clear=True
                )

    def p_table(self):
        """

        :return:
        """
        if self.d.df is None:
            return
        if self.df_table is None:
            return
        self.df_table.setDfData(self.d.df)
        # self.df_table.pdToQTableWidget()

    @GraphRangeSignal
    def wPlot(self):
        """
        绘制每个Pin的X状态轴
        以及每个周期的Y轴
        Y不要超过100个
        """
        if self.d.df is None:
            return
        for v_line in self.chart_v_lines:
            self.vb.removeItem(v_line)
        self.chart_v_lines.clear()
        diff = int((self.p_range.x_max - self.p_range.x_min))
        steps = 1 + diff // self.d.waveTimingCycle
        step = 1 + steps // 100
        t_waveTimingCycle = step * self.d.waveTimingCycle  # 抽样后的间距

        t_min = (int(self.p_range.x_min) // t_waveTimingCycle) * t_waveTimingCycle

        for index in range(int(diff / t_waveTimingCycle) + 2):
            v_line = InfiniteLine(angle=90, movable=False, pen=(250, 194, 5, 130))
            self.vb.addItem(v_line, ignoreBounds=True)
            v_line.setPos(t_min + index * t_waveTimingCycle)
            self.chart_v_lines.append(v_line)

    @GraphRangeSignal
    def lPlot(self):
        """
        显示每个输入输出的X(中间)
        就显示一次就行
        """
        for key, value in self.d.PinList.items():
            l_line = InfiniteLine(angle=0, movable=False, pen=(19, 234, 201, 130))
            self.vb.addItem(l_line, ignoreBounds=True)
            l_line.setPos(self.y_local_cache[key] * self.d.YStep + 5)

            pen = QPen(QColor(54, 55, 55))
            pen.setWidthF(0.5)  # 设置线条粗细为3像素

            l_line_inter = InfiniteLine(angle=0, movable=False, pen=pen)
            self.vb.addItem(l_line_inter, ignoreBounds=True)
            l_line_inter.setPos(self.y_local_cache[key] * self.d.YStep)

            l_line_inter1 = InfiniteLine(angle=0, movable=False, pen=pen)
            self.vb.addItem(l_line_inter1, ignoreBounds=True)
            l_line_inter1.setPos(self.y_local_cache[key] * self.d.YStep + self.d.YStep / 2)

    @GraphRangeSignal
    def plot(self):
        """
        1. 预设Wvt所用的Plot
        2. Plot要先运行过
        3. Scope是黄色
        """

        if not self.change:
            self.set_default_range()

        for key, plot in self.d.WvtPlotMap.items():
            plot.clear()
            # plot.hide()
        TimeAxis.TimingSet = self.d.TimingSet
        int_x_min = int(self.p_range.x_min)
        int_x_max = int(self.p_range.x_max)
        if int_x_min < 0:
            int_x_min = 0
        if int_x_max > len(self.d.df):
            int_x_max = len(self.d.df)
        df = self.d.df[int_x_min: int_x_max]
        self.d.list_bins = int(len(df) // self.d.GraphPlotScatterSimple)
        if self.d.list_bins % 2 == 0:
            self.d.list_bins += 1  # 不能为偶数位, 不然时钟会出错
        df = df[::self.d.list_bins]
        # if self.d.list_bins != 0:
        #     df = df[::self.d.list_bins]
        # else:
        #     self.d.list_bins = 1
        x = np.arange(0, len(df) + 1)
        i_color = (0, 255, 0)
        o_color = (255, 0, 0)
        for index, (key, value) in enumerate(self.d.PinList.items()):
            if key not in self.d.WvtPlotMap:
                self.y_local_cache[key] = index
                color = i_color  # 绿色
                if value == PinType.IO:
                    i_key = key + "_I"
                    i_plot = PlotDataItem(
                        pen=i_color, stepMode="center"
                    )
                    i_plot.setData(
                        x=(x + self.d.PinOffset.get(i_key, 0)) * self.d.list_bins,
                        y=np.array(df[i_key] + index * self.d.YStep),
                    )
                    self.d.WvtPlotMap[i_key] = i_plot
                    self.pw.addItem(i_plot)

                    o_key = key + "_O"
                    o_plot = PlotDataItem(
                        pen=o_color, stepMode="center"
                    )
                    o_plot.setData(
                        x=(x + self.d.PinOffset.get(o_key, 0)) * self.d.list_bins,
                        y=np.array(df[o_key] + index * self.d.YStep),
                    )
                    self.d.WvtPlotMap[o_key] = o_plot
                    self.pw.addItem(o_plot)
                    # 特殊操作
                    continue
                if value == PinType.OUT:
                    color = o_color  # 红色
                plot = PlotDataItem(
                    pen=color, stepMode="center"
                )
                plot.setData(
                    x=(x + self.d.PinOffset.get(key, 0)) * self.d.list_bins,
                    y=np.array(df[key] + index * self.d.YStep),
                )
                self.d.WvtPlotMap[key] = plot
                self.pw.addItem(plot)
            else:
                plot = self.d.WvtPlotMap[key]
                plot.setData(
                    x=(x + self.d.PinOffset.get(key, 0)) * self.d.list_bins + int_x_min,
                    y=np.array(df[key] + index * self.d.YStep),
                    clear=True
                )
                # plot.show()

        ticks = [(idx * self.d.YStep + 0.5, label) for idx, label in enumerate(self.d.fPinList.keys())]
        self.pw.getAxis("left").setTicks((ticks, []))

        if not self.change:
            self.vb.setXRange(self.p_range.x_min, self.p_range.x_max)

            x_pec_5 = len(self.d.df) * 0.1
            y_pec_5 = len(self.d.fPinList) * 0.1
            if y_pec_5 < 1:
                y_pec_5 = 1
            self.vb.setLimits(xMin=-x_pec_5, xMax=len(self.d.df) + x_pec_5, yMin=-y_pec_5 * self.d.YStep,
                              yMax=len(self.d.fPinList) * self.d.YStep + y_pec_5 * self.d.YStep)
            self.p_range.x_min, self.p_range.x_max = self.bottom_axis.range
            self.change = True

    def set_range_data_to_chart(self, a, ax) -> bool:
        data_min, data_max = self.get_cache_range(self.sig)
        percent = (ax[self.sig][1] - ax[self.sig][0]) / (data_max - data_min)
        if 0.85 < percent < 1.15:
            return False
        if (self.p_range.x_max - self.p_range.x_min) / self.d.list_bins > self.d.ShowFcRange:
            self.select_bin_dialog_button.setEnabled(False)
        else:
            self.select_bin_dialog_button.setEnabled(True)
        self.set_p_range(ax)
        self.plot()
        self.fPlot()
        self.wPlot()
        if self.wvt_text is None:
            self.wvt_text = TextItem(anchor=(-0.3, 0.5), border='w', fill=(0, 0, 255, 100))
            self.pw.addItem(self.wvt_text)
        # 在最左边的1/4高和1/8宽的地方
        x = self.p_range.x_min + (self.p_range.x_max - self.p_range.x_min) / 8
        y = self.p_range.y_max - (self.p_range.y_max - self.p_range.y_min) / 6
        self.wvt_text.setPos(x, y)
        if not self.line_init:
            self.init_movable_line()
            self.line_init = True
        return True

    def _select_bin_dialog_button(self):
        """
        将实际波形显示到数据中, 只显示有Fail的数据的波形
        将FailCycle的实际的数据转到原始Data中
        1. 获取原始Data当前Local的数据 Df
        2. 将实际Fail数据的输出转到 Df数据中修改
        3. TODO: 暂时有多个LoopIndex的数据
        4. 可以加入LoopIndex选取的功能
        :return:
        """
        if self.d.f_df is None:
            return
        for key, plot in self.d.rWvtPlotMap.items():
            plot.clear()
        int_x_min = int(self.p_range.x_min)
        int_x_max = int(self.p_range.x_max)
        if int_x_min < 0:
            int_x_min = 0
        if int_x_max > len(self.d.df):
            int_x_max = len(self.d.df)
        fl_df = self.d.df[int_x_min: int_x_max][::self.d.list_bins].copy()
        color = (255, 255, 0)  # 黄色
        x = np.arange(0, len(fl_df) + 1)
        for pin_key, df in self.d.f_dfDict.items():
            if pin_key not in self.d.PinList:
                continue
            # pin_type = self.d.PinList[pin_key]
            # if pin_type == PinType.IO:
            #     pin_key = pin_key + "_O"
            y_index = self.y_local_cache[pin_key]
            f_wvt_key = "{}@{}".format(pin_key, "f_wvt")
            df = df[df["LoopIndex"] == self.loop_index.value()]
            df_out_h = df[df["Output"] == 1]
            df_out_l = df[df["Output"] == 0]
            y_df = fl_df[pin_key]
            y_df.loc[y_df.index.isin(df_out_h.FailCycle)] = int(WaveTable.OUTPUT_H)
            if self.d.CycleMode == AteCycleMode.X8:
                y_df.loc[y_df.index.isin(df_out_h.FailCycle + self.x8_mask_add.value())] = int(
                    WaveTable.OUTPUT_H)  # X8改X4
            y_df.loc[y_df.index.isin(df_out_l.FailCycle)] = int(WaveTable.OUTPUT_L)
            if self.d.CycleMode == AteCycleMode.X8:
                y_df.loc[y_df.index.isin(df_out_l.FailCycle + self.x8_mask_add.value())] = int(
                    WaveTable.OUTPUT_L)  # X8改X4
            if f_wvt_key not in self.d.rWvtPlotMap:
                plot = PlotDataItem(
                    pen=color, stepMode="center"
                )
                plot.setData(
                    x=(x + self.d.PinOffset.get(pin_key, 0)) * self.d.list_bins + int_x_min,
                    y=np.array(y_df + y_index * self.d.YStep - int(WaveTable.OUTPUT_H) - 1),
                )
                self.d.rWvtPlotMap[f_wvt_key] = plot
                self.pw.addItem(plot)
            else:
                plot = self.d.rWvtPlotMap[f_wvt_key]
                plot.setData(
                    x=(x + self.d.PinOffset.get(pin_key, 0)) * self.d.list_bins + int_x_min,
                    y=np.array(y_df + y_index * self.d.YStep - int(WaveTable.OUTPUT_H) - 1),
                    clear=True
                )
